package org.apache.lucene.ocean.util;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.regex.Pattern;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.ocean.LogDirectory;
import org.apache.lucene.ocean.database.OceanObject;
import org.apache.lucene.search.ExtendedFieldCache;
import org.apache.lucene.search.ExtendedFieldCache.LongParser;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.store.RAMFile;
import org.apache.lucene.store.RAMInputStream;

/**
 * Utilities used by the rest of the system.
 */
public class Util {
  public static TimeZone UTC = TimeZone.getTimeZone("UTC");
  public final static Pattern splitPattern = Pattern.compile(",| ");
  private static ThreadLocalDateFormat dateFormatThreadLocal = new ThreadLocalDateFormat();
  
  public static String toIdVersionString(long id, long version) {
    return id+"_"+version;
  }
  
  public static String dumpDocuments(IndexReader reader) throws Exception {
    int maxDoc = reader.maxDoc();
    StringBuilder b = new StringBuilder();
    for (int x=0; x < maxDoc; x++) {
      if (reader.isDeleted(x)) continue;
      Document doc = reader.document(x);
      List list = doc.getFields();
      Iterator iterator = list.iterator();
      while (iterator.hasNext()) {
        Fieldable fieldable = (Fieldable)iterator.next();
        String key = fieldable.name();
        String value = fieldable.stringValue();
        b.append(key);
        b.append(":");
        Object obj = OceanObject.toExternalObject(key, value);
        b.append(obj.toString());
        if (iterator.hasNext()) {
          b.append(",");
        }
      }
    }
    return b.toString();
  }
  
  /**
   * Decodes a possibly encoded string to a long.
   *
   */
  public static class EncodedLongParser implements LongParser {
    public long parseLong(String string) {
      return NumberUtils.sortableString2Long(string);
      /**
      try {
        Object o = OceanObject.toExternalObject(string);
        if (o instanceof Number) {
          Number n = (Number) o;
          return n.longValue();
        } else
          return Long.MIN_VALUE;
      } catch (ParseException pe) {
        throw new RuntimeException(pe);
      }
      **/
    }
  }

  public static String formatSnapshotId(BigDecimal id) {
    DecimalFormat format = new DecimalFormat("##0.00");
    return format.format(id);
  }
  
  /**
   * Get the minimum value in a collection.  Return null if the size is zero.
   * @param <T>
   * @param collection
   * @return
   */
  public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> collection) {
    if (collection.size() == 0)
      return null;
    return Collections.min(collection);
  }

  /**
   * Handles size 0 collections returns null
   */
  public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> collection) {
    if (collection.size() == 0)
      return null;
    return Collections.max(collection);
  }

  public static void touchFile(String file, Directory directory) throws IOException {
    IndexOutput output = directory.createOutput(file);
    output.close();
  }

  public static String getString(String file, LogDirectory directory) throws IOException {
    try {
      RandomAccessFile input = directory.openInput(file);
      if (input.length() == 0)
        return null;
      byte[] bytes = new byte[(int) input.length()];
      input.read(bytes);
      return new String(bytes, "UTF-8");
    } catch (Throwable ioException) {
      IOException newIOException = new IOException("file: " + file);
      newIOException.initCause(ioException);
      throw newIOException;
    }
  }

  public static void save(String string, String file, LogDirectory directory) throws IOException {
    byte[] bytes = string.getBytes("UTF-8");
    RandomAccessFile output = directory.getOutput(file, true);
    output.write(bytes);
    output.getFD().sync();
    output.close();
  }

  public static String getString(String file, Directory directory) throws IOException {
    IndexInput input = directory.openInput(file);
    byte[] bytes = new byte[(int) input.length()];
    input.readBytes(bytes, 0, bytes.length);
    return new String(bytes, "UTF-8");
  }

  public static void save(String string, String file, Directory directory) throws IOException {
    byte[] bytes = string.getBytes("UTF-8");
    IndexOutput output = directory.createOutput(file);
    output.writeBytes(bytes, bytes.length);
    output.flush();
    output.close();
  }

  public static void copy(IndexInput input, IndexOutput output, byte[] buffer) throws IOException {
    long len = input.length();
    long readCount = 0;
    while (readCount < len) {
      int toRead = readCount + buffer.length > len ? (int) (len - readCount) : buffer.length;
      input.readBytes(buffer, 0, toRead);
      output.writeBytes(buffer, toRead);
      readCount += toRead;
    }
  }

  public static <K,V> V getLastValue(SortedMap<K,V> map) {
    if (map.size() == 0)
      return null;
    return map.get(map.lastKey());
  }

  public static void setValue(String name, long value, Document document) {
    String encoded = longToEncoded(value);
    document.add(new Field(name, encoded, Field.Store.YES, Field.Index.UN_TOKENIZED));
  }

  public static boolean mkdir(File dir) {
    if (!dir.exists()) {
      return dir.mkdirs();
    }
    return false;
  }

  public static String longToEncoded(long value) {
    return long2sortableStr(value);
  }

  public static long longFromEncoded(String string) {
    return SortableStr2long(string, 0, 5);
  }

  public static String long2sortableStr(long val) {
    char[] arr = new char[5];
    long2sortableStr(val, arr, 0);
    return new String(arr, 0, 5);
  }

  // uses binary representation of an int to build a string of
  // chars that will sort correctly. Only char ranges
  // less than 0xd800 will be used to avoid UCS-16 surrogates.
  // we can use the lowest 15 bits of a char, (or a mask of 0x7fff)
  public static int long2sortableStr(long val, char[] out, int offset) {
    val += Long.MIN_VALUE;
    out[offset++] = (char) (val >>> 60);
    out[offset++] = (char) (val >>> 45 & 0x7fff);
    out[offset++] = (char) (val >>> 30 & 0x7fff);
    out[offset++] = (char) (val >>> 15 & 0x7fff);
    out[offset] = (char) (val & 0x7fff);
    return 5;
  }

  public static long SortableStr2long(String sval, int offset, int len) {
    long val = (long) (sval.charAt(offset++)) << 60;
    val |= ((long) sval.charAt(offset++)) << 45;
    val |= ((long) sval.charAt(offset++)) << 30;
    val |= sval.charAt(offset++) << 15;
    val |= sval.charAt(offset);
    val -= Long.MIN_VALUE;
    return val;
  }

  public static int getDoc(String fieldName, long value, IndexReader indexReader) throws IOException {
    String encoded = longToEncoded(value);
    return getTermDoc(new Term(fieldName, encoded), indexReader);
  }

  public static int getTermDoc(Term term, IndexReader indexReader) throws IOException {
    TermDocs docs = indexReader.termDocs(term);
    try {
      if (docs.next()) {
        return docs.doc();
      }
    } finally {
      docs.close();
    }
    return -1;
  }

  public static List<Integer> getTermDocs(Term term, IndexReader indexReader) throws IOException {
    List<Integer> list = new ArrayList<Integer>();
    TermDocs docs = indexReader.termDocs(term);
    try {
      while (docs.next()) {
        list.add(docs.doc());
      }
    } finally {
      docs.close();
    }
    return list;
  }

  public static long getSize(Directory directory) throws IOException {
    long total = 0;
    for (String file : directory.list()) {
      total += directory.fileLength(file);
    }
    return total;
  }

  public static void copyRamFile(RAMFile ramFile, OutputStream os) throws IOException {
    int numBuffers = ramFile.numBuffers();
    long length = ramFile.getLength();
    int bytesLength;
    int numWritten = 0;
    for (int x = 0; x < numBuffers; x++) {
      byte[] buffer = ramFile.getBuffer(x);
      if (buffer.length + numWritten > length)
        bytesLength = (int) (length - numWritten);
      else
        bytesLength = buffer.length;
      os.write(buffer, 0, bytesLength);
    }
  }

  public static void copy(RAMDirectory src, Directory dest) throws IOException {
    final String[] files = src.list();

    if (files == null)
      throw new IOException("cannot read directory " + src + ": list() returned null");

    for (int i = 0; i < files.length; i++) {
      IndexOutput os = null;
      RAMInputStream is = null;
      try {
        // create file in dest directory
        os = dest.createOutput(files[i]);
        // read current file
        is = (RAMInputStream) src.openInput(files[i]);
        RAMFile ramFile = is.getRAMFile();
        int numBuffers = ramFile.numBuffers();
        long length = ramFile.getLength();
        int bytesLength;
        int numWritten = 0;
        for (int x = 0; x < numBuffers; x++) {
          byte[] buffer = ramFile.getBuffer(x);
          if (buffer.length + numWritten > length)
            bytesLength = (int) (length - numWritten);
          else
            bytesLength = buffer.length;
          os.writeBytes(buffer, bytesLength);
        }
      } finally {
        // graceful cleanup
        try {
          if (os != null)
            os.close();
        } finally {
          if (is != null)
            is.close();
        }
      }
    }
  }

  public static void copy(InputStream is, RandomAccessFile ras, byte[] buf) throws IOException {
    int numRead;
    while ((numRead = is.read(buf)) >= 0) {
      ras.write(buf, 0, numRead);
    }
  }

  public static File getDirectory(File root, String path) {
    File file = new File(root, path);
    if (!file.exists()) {
      file.mkdirs();
    }
    return file;
  }

  public static DateFormat getThreadLocalDateFormat() {
    return dateFormatThreadLocal.get();
  }

  public static String formatDate(Date date) {
    DateFormat dateFormat = getThreadLocalDateFormat();
    return dateFormat.format(date);
  }

  public static Date parseDate(String string) throws ParseException {
    DateFormat dateFormat = getThreadLocalDateFormat();
    return dateFormat.parse(string);
  }

  private static class ThreadLocalDateFormat extends ThreadLocal<DateFormat> {
    DateFormat proto;

    public ThreadLocalDateFormat() {
      super();
      SimpleDateFormat tmp = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
      tmp.setTimeZone(UTC);
      proto = tmp;
    }

    protected DateFormat initialValue() {
      return (DateFormat) proto.clone();
    }
  }

  public static long[] getFieldCacheLong(String field, IndexReader indexReader) throws IOException {
    return ExtendedFieldCache.EXT_DEFAULT.getLongs(indexReader, field, new EncodedLongParser());
  }

  public static boolean isTrue(String string) {
    return StringUtils.equalsIgnoreCase("true", string);
  }

  public static Object construct(Object parameter, Class clazz) throws Exception {
    Constructor constructor = clazz.getDeclaredConstructor(parameter.getClass());
    try {
      return constructor.newInstance(parameter);
    } catch (InvocationTargetException invocationTargetException) {
      Throwable cause = invocationTargetException.getCause();
      if (cause instanceof Exception)
        throw (Exception) cause;
      else
        throw invocationTargetException;
    }
  }

  public static Long parseLong(String value) {
    if (StringUtils.isBlank(value)) {
      return null;
    }
    try {
      return new Long(value);
    } catch (Throwable ex) {
      return null;
    }
  }

  public static URL parseURL(String string) throws MalformedURLException {
    if (StringUtils.isBlank(string)) {
      return null;
    }
    return new URL(string);
  }

  public static List<String> readLines(Reader reader) throws Exception {
    return IOUtils.readLines(reader);
  }

  public static IOException asIOException(String message, Throwable throwable) throws IOException {
    IOException ioException = new IOException(message);
    ioException.initCause(throwable);
    return ioException;
  }

  public static IOException asIOException(Throwable throwable) {
    if (throwable instanceof IOException) {
      return (IOException) throwable;
    }
    IOException ioException = new IOException(throwable.getMessage());
    ioException.initCause(throwable);
    return ioException;
  }

  public static void copy(InputStream is, OutputStream os, byte[] buf) throws IOException {
    int numRead;
    while ((numRead = is.read(buf)) >= 0) {
      os.write(buf, 0, numRead);
    }
  }

  public static int[] toIntArray(List<Integer> list) {
    int size = list.size();
    int[] array = new int[size];
    int x = 0;
    for (int i : list) {
      array[x] = i;
      x++;
    }
    return array;
  }

  public static List<URL> loadUrls(File file) throws IOException {
    List<String> lines = IOUtils.readLines(new FileReader(file));
    List<URL> urls = new ArrayList<URL>();
    for (String line : lines) {
      urls.add(new URL(line));
    }
    return urls;
  }

  /**
   * public static HttpParameters toHttpParameters(HttpServletRequest request) {
   * try { if (StringUtils.equalsIgnoreCase("post", request.getMethod())) {
   * HttpParameters parameters = new HttpParameters(); URL url = new
   * URL(request.getRequestURL().toString()); CGIParser cgiParser = new
   * CGIParser(url.toString(), "UTF-8"); for (String name :
   * cgiParser.getParameterNameList()) { for (String value :
   * cgiParser.getParameterValues(name)) { parameters.add(name, value); } }
   * return parameters; } } catch (Exception exception) { throw new
   * RuntimeException(exception); } if (StringUtils.equalsIgnoreCase("get",
   * request.getMethod())) { HttpParameters parameters = new HttpParameters();
   * Enumeration paramEnum = request.getParameterNames(); while
   * (paramEnum.hasMoreElements()) { String name = (String)
   * paramEnum.nextElement(); String[] array = request.getParameterValues(name);
   * if (array != null && array.length > 0) { for (String value : array) {
   * parameters.add(name, value); } } } return parameters; } throw new
   * RuntimeException("unknown http method " + request.getMethod()); }
   */
  public static Long getNextServerSequence(Long value, int serverNumber) {
    if (value == null) {
      return new Long(serverNumber);
    }
    Long i = null;
    if (value > 99) {
      String string = value.toString();
      String substring = string.substring(0, string.length() - 2);
      i = new Long(substring + "00");
    } else {
      i = new Long(0);
    }
    long v = i + serverNumber;
    return v + 100;
  }

  public static int getServerNumber(BigInteger id) {
    String string = id.toString();
    String substring = string.substring(string.length() - 2, string.length());
    return Integer.parseInt(substring);
  }

  public static SortedSet<String> splitToSortedSet(String string) {
    if (StringUtils.isBlank(string)) {
      return null;
    }
    String[] array = splitPattern.split(string.trim(), 0);
    TreeSet<String> sortedSet = new TreeSet<String>();
    for (int x = 0; x < array.length; x++) {
      sortedSet.add(array[x]);
    }
    return sortedSet;
  }

  public static File getAppServerHome() {
    return new File(System.getProperty("catalina.home"));
  }

  public static File getHomeDirectory(String name, File defaultDirectory) throws Exception {
    String value = System.getenv(name);
    if (value != null)
      return new File(value);
    value = System.getProperty(name);
    if (value != null)
      return new File(value);
    Context context = (Context) new InitialContext().lookup("java:comp/env");
    try {
      String string = (String) context.lookup(name);
      if (StringUtils.isNotBlank(value))
        return new File(value);
    } catch (NameNotFoundException nameNotFoundException) {
    }
    defaultDirectory.mkdirs();
    return defaultDirectory;
  }

  public static Object getFirst(List list) {
    Iterator iterator = list.iterator();
    if (iterator.hasNext()) {
      return iterator.next();
    } else {
      return null;
    }
  }

  public static Map<String,String> toMapExcept(org.jdom.Element element, String exceptName) {
    Map<String,String> map = new HashMap<String,String>();
    for (Object object : element.getAttributes()) {
      org.jdom.Attribute attribute = (org.jdom.Attribute) object;
      String name = attribute.getName();
      if (!StringUtils.equals(name, exceptName)) {
        map.put(name, attribute.getValue());
      }
    }
    return map;
  }
}
